package edu.xhu.minio.utils;

import edu.xhu.minio.clients.BiliMinIOClient;
import edu.xhu.minio.config.BiliMinIOConfigProperties;
import edu.xhu.model.minio.constant.MinioConstants;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@RequiredArgsConstructor
@Component
@EnableConfigurationProperties({BiliMinIOConfigProperties.class})
public class BiliMinioUtils {

    private final RedisTemplate<String, String> redisTemplate;

    //已经内置好分片桶，正式桶
    private final BiliMinIOConfigProperties biliMinIOConfigProperties;

    //存了5个自定义minio客户端BiliMinIOClient
    //<服务器ip地址：对应服务器的自定义minio客户端>
    private final Map<String, BiliMinIOClient> biliMinIOClientMap; //一组自定义minio客户端

    // uploadChunk 方法中的锁对象变量
    private final Object uploadChunkLock = new Object();

    /**
     * @param objectName 图片路径
     * @param file       图片文件流
     * @return 图片的URL，如果为空，说明上传失败
     */
    public String uploadImageFile(String objectName, MultipartFile file) {
        //图片的URL
        String imageUrl = null;

        MinioClient minioClient = null;

        //桶路径
        String bucketName = biliMinIOConfigProperties.getBucket();

        //拿到磁盘剩余空间最大的服务器ip
        String host = this.findServerWithMaxDiskSpaceIP();

        //获取对应ip的minioClient客户端
        if (host != null)
            minioClient = biliMinIOClientMap.get(host).getMinioClient();

        if (minioClient != null) {
            try {
                //上传图片
                minioClient.putObject(PutObjectArgs.builder()
                        .object(objectName)
                        .contentType("image/jpg")
                        .bucket(bucketName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .build());

                //拼接图片的路径
                imageUrl = biliMinIOClientMap.get(host).getReadRootPath()
                        .concat("/")
                        .concat(bucketName)
                        .concat("/")
                        .concat(objectName);

                return imageUrl;
            } catch (Exception e) {
                //上传失败
                log.error("图片上传失败!|参数：bucketName:{},objectName:{}|异常:{}", bucketName, objectName, e);
                return imageUrl;
            }
        }
        return imageUrl;
    }


    /**
     * 文件分块上传
     *
     * @param objectName 对象名称-视频的md5
     * @param chunkIndex 分片索引
     * @param file       文件
     */
    public Boolean uploadChunk(String objectName, Integer chunkIndex, MultipartFile file) {
        //根据视频的md5从redis中获取该视频的存储地址（ip地址）
        String host = redisTemplate.opsForValue().get(objectName);

        //redis中未保存该视频的存储位置
        if (host == null) {
            // String hostToSet = this.findServerWithMaxDiskSpaceIP();
            synchronized (uploadChunkLock) {

                //根据视频的md5从redis中获取该视频的存储地址（ip地址）
                host = redisTemplate.opsForValue().get(objectName);

                if (host == null) {
                    //拿到连接数最小的 host
                    String hostToSet = redisTemplate.opsForZSet().range(MinioConstants.MIN_LOAD_BALANBER_ZSET, 0, 0).stream().findFirst().orElse(null);

                    if (hostToSet == null)
                        return false;

                    boolean isSetSuccessful = redisTemplate.opsForValue().setIfAbsent(objectName, hostToSet);

                    if (isSetSuccessful) {
                        host = hostToSet;
                        // 给指定 host 负载分数加 1
                        redisTemplate.opsForZSet().incrementScore(MinioConstants.MIN_LOAD_BALANBER_ZSET, hostToSet, 1);
                    } else
                        host = redisTemplate.opsForValue().get(objectName);
                }
            }
        }

        if (host != null) {
            try {
                if (chunkIndex != null) {
                    objectName = objectName.concat("/").concat(Integer.toString(chunkIndex));
                }

                MinioClient minioClient = biliMinIOClientMap.get(host).getMinioClient();
                String chunkBucketName = biliMinIOConfigProperties.getChunkBucket();

                // 写入文件
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(chunkBucketName)
                        .object(objectName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());

                log.debug("上传到minio文件|uploadFile|参数：chunkBucketName：{}，objectName：{}，chunkIndex：{}"
                        , chunkBucketName, objectName, chunkIndex);

                return true;
            } catch (Exception e) {
                log.error("文件上传到Minio异常|参数：chunkBucketName:{},objectName:{},chunkIndex:{}|异常:{}", biliMinIOConfigProperties.getChunkBucket(), objectName, chunkIndex, e);
                return false;
            }
        }

        return false;
    }

    /**
     * 文件分块合并，已经内置好分片桶，正式桶
     *
     * @param objectName 正式桶中的目录路径
     * @param md5        视频的 md5 码
     * @return 返回视频URL，返回如果为空，说明合并分片失败
     */
    public String composeChunks(String objectName, String md5) {
        //视频URL
        String videoUrl = null;

        MinioClient minioClient = null;

        // 计算视频已经上传成功的分片数量
        int chunkCount = this.countChunksOfVideo(md5);

        //根据视频的md5从redis中获取该视频的存储地址（ip地址）
        String host = redisTemplate.opsForValue().get(md5);

        if (host != null)
            minioClient = biliMinIOClientMap.get(host).getMinioClient();

        if (chunkCount > 0 && minioClient != null) {
            String bucketName = biliMinIOConfigProperties.getBucket(); //正式桶
            String chunkBucketName = biliMinIOConfigProperties.getChunkBucket(); //分片桶

            //从分片缓存目录获取所有分片
            List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i)
                    .limit(chunkCount)  //分片数量
                    .map(i -> ComposeSource.builder()
                            .bucket(chunkBucketName) //分片存储桶名称
                            .object(md5.concat("/").concat(Integer.toString(i)))
                            .build())
                    .collect(Collectors.toList());

            //开始合并分片
            try {
                ObjectWriteResponse response = minioClient.composeObject(
                        ComposeObjectArgs.builder()
                                .bucket(bucketName)
                                .object(objectName)
                                .sources(sourceObjectList)
                                .build());
            } catch (Exception e) {
                log.error("Minio文件按合并异常!|参数：bucketName:{},objectName:{}|异常:{}", bucketName, objectName, e);
                return videoUrl;
            }

            //清理该视频所有缓存的临时分片文件
            this.removeTemporaryChunks(md5);

            synchronized (uploadChunkLock) {
                //清理redis中的缓存
                redisTemplate.delete(md5);
                // 给指定 host 负载分数减 1
                redisTemplate.opsForZSet().incrementScore(MinioConstants.MIN_LOAD_BALANBER_ZSET, host, -1);
            }

            //拼接视频URL
            videoUrl = biliMinIOClientMap.get(host).getReadRootPath()
                    .concat("/")
                    .concat(bucketName)
                    .concat("/")
                    .concat(objectName);

            return videoUrl;
        }

        return videoUrl;
    }

    /**
     * 删除某个视频所有缓存的分片文件
     *
     * @param prefixObjectName 视频的md5码
     */
    public void removeTemporaryChunks(String prefixObjectName) {
        MinioClient minioClient = null;

        String chunkBucketName = biliMinIOConfigProperties.getChunkBucket(); //分片桶

        //根据视频的md5从redis中获取该视频的存储地址（ip地址）
        String host = redisTemplate.opsForValue().get(prefixObjectName);

        if (host != null)
            minioClient = biliMinIOClientMap.get(host).getMinioClient();

        if (minioClient != null) {
            try {
                Iterable<Result<Item>> objects = minioClient.listObjects(
                        ListObjectsArgs.builder()
                                .bucket(chunkBucketName)
                                .prefix(prefixObjectName.concat("/"))
                                .recursive(true)
                                .build());

                //拿到该视频所有缓存分片的路径
                List<String> objectsToDelete = new ArrayList<>();
                for (Result<Item> result : objects) {
                    Item item = result.get();
                    objectsToDelete.add(item.objectName());
                }

                //遍历路径，删除该视频所有缓存的分片
                for (String objectName : objectsToDelete) {
                    minioClient.removeObject(
                            RemoveObjectArgs.builder()
                                    .bucket(chunkBucketName)
                                    .object(objectName)
                                    .build());
                }
            } catch (Exception e) {
                log.error(e.getMessage());
            }
        }
    }

    /**
     * 计算视频已经上传成功的分块数量
     *
     * @param prefixObjectName 视频的 md5
     * @return 上传成功的分片数量，返回-1表示异常
     */
    public Integer countChunksOfVideo(String prefixObjectName) {
        //根据视频的md5从redis中获取该视频的存储地址（ip地址）
        String host = redisTemplate.opsForValue().get(prefixObjectName);

        if (host == null)
            return -1;

        MinioClient minioClient = biliMinIOClientMap.get(host).getMinioClient();

        if (minioClient != null) {
            String chunkBucketName = biliMinIOConfigProperties.getChunkBucket();
            try {
                Iterable<Result<Item>> results = minioClient.listObjects(
                        ListObjectsArgs.builder()
                                .bucket(chunkBucketName)
                                .prefix(prefixObjectName.concat("/"))
                                .build());

                int chunkCount = 0;

                for (Result<Item> result : results) {
                    Item item = result.get();
                    if (!item.isDir()) {
                        chunkCount++;
                    }
                }

                return chunkCount;

            } catch (Exception e) {
                log.error("Minio获取文件流异常!|参数：chunkBucketName:{},prefixObjectName:{}|异常:{}", chunkBucketName, prefixObjectName, e);
            }
        }

        return -1;
    }


    /**
     * @return 剩余磁盘空间最大的服务器ip地址，返回空说明获取失败
     */
    public String findServerWithMaxDiskSpaceIP() {
        String maxDiskSpaceServerIP = null;
        double maxDiskSpace = -1;


        for (Map.Entry<String, BiliMinIOClient> entry : biliMinIOClientMap.entrySet()) {
            //判断SSH连接是否已经失效
            if (!entry.getValue().getBiliSSHCilent().isConnected())
                entry.getValue().getBiliSSHCilent().connect();

            if (entry.getValue().getBiliSSHCilent().getAvailableDiskSpaceInGB() > maxDiskSpace
                    && entry.getValue().getBiliSSHCilent().isConnected()) {
                maxDiskSpace = entry.getValue().getBiliSSHCilent().getAvailableDiskSpaceInGB();
                maxDiskSpaceServerIP = entry.getValue().getBiliSSHCilent().getHost();
            }
        }
        return maxDiskSpaceServerIP;
    }

    /**
     * 测试方法
     */
    public void helloBiliMinioUtils() {
        // 使用for-each循环遍历values
        for (BiliMinIOClient value : biliMinIOClientMap.values()) {
            System.out.println("session: " + value.getBiliSSHCilent().isConnected());
            System.out.println("host: " + value.getBiliSSHCilent().getHost());
            System.out.println("磁盘剩余空间: " + value.getBiliSSHCilent().getAvailableDiskSpaceInGB() + "GB");
            System.out.println();
        }

        // 使用for-each循环遍历keySet
        for (String key : biliMinIOClientMap.keySet()) {
            System.out.println("key: " + key);
            System.out.println("host: " + biliMinIOClientMap.get(key).getBiliSSHCilent().getHost());
        }
    }
}
